﻿//===================================================================================
// Copyright (c) Ryan Garrett.  All rights reserved.
// THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY
// OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT
// LIMITED TO THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
// FITNESS FOR A PARTICULAR PURPOSE.
//===================================================================================
//WRITE: Remember to document that if DEBUG is not set, ValidateModelProperties will be false

using Caffeine.Core.Metadata;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.Serialization;

namespace Caffeine.Core
{
    /// <summary>
    /// This is the default model factory that is used by the Caffeine.Core.Creator static class. If you want to provide an alternate
    /// implementation of a model factory, it is recommended that you inherit from this class as a starting point. If the #DEBUG flag
    /// is set, this factory will validate all model properties to make sure they're registered correctly.
    /// </summary>
    public class ModelFactory : IModelFactory
    {
        #region Constructors

        /// <summary>
        /// Constructor.
        /// </summary>
        public ModelFactory()
        {
#if DEBUG
            ValidateModelProperties = true;
#endif
            PropertyChangeCounter = new PropertyChangeCounter();
            ModelExtensions = new Dictionary<Type, Func<ModelBase, Object>>();
        }

        #endregion

        #region Public

        /// <summary>
        /// Gets the dictionary containing delegates used to generate extensions for models
        /// </summary>
        public IDictionary<Type, Func<ModelBase, Object>> ModelExtensions
        {
            get;
            private set;
        }

        /// <summary>
        /// Gets or sets whether or not model properties are validated to be registered correctly as each model is created. If #DEBUG
        /// is specified, this will be set to true, otherwise it will be false
        /// </summary>
        public Boolean ValidateModelProperties
        {
            get;
            set;
        }

        /// <summary>
        /// Gets or sets whether or not this model factory assigns each model's UniqueId property as its created.
        /// Defaults to false. Must be set to true to allow merge operations.
        /// </summary>
        public Boolean AssignModelUniqueId
        {
            get;
            set;
        }

        /// <summary>
        /// Gets or sets the property change counter that is assigned to each model as it is created
        /// </summary>
        public IPropertyChangeCounter PropertyChangeCounter
        {
            get;
            set;
        }

        /// <summary>
        /// Gets or sets the exception handler for model commands
        /// </summary>
        public ICommandExceptionHandler CommandExceptionHandler
        {
            get;
            set;
        }

        /// <summary>
        /// Creates a model
        /// </summary>
        /// <typeparam name="T">Type of model to create. Must have a default constructor.</typeparam>
        /// <returns>A new model</returns>
        public virtual T CreateModel<T>() where T : ModelBase, new()
        {
            return CreateModel<T>(null, null);
        }

        public virtual T CreateModel<T>(ModelMetadata metadata)
            where T : ModelBase, new()
        {
            return CreateModel<T>(metadata, null);
        }

        public virtual ModelBase CreateModel(Type modelType)
        {
            MethodInfo method = typeof(ModelFactory).GetTypeInfo().DeclaredMethods.Single(c => c.Name == "InternalCreateModel");
            method = method.MakeGenericMethod(modelType);
            return method.Invoke(this, new object[] { null, null }) as ModelBase;
        }

        public virtual T CreateModel<T>(ModelMetadata metadata, ModelPropertyBase parentProperty)
            where T : ModelBase, new()
        {
            return InternalCreateModel<T>(metadata, parentProperty);   
        }

        public Byte[] SerializeModel<T>(T model, IEnumerable<Type> knownTypes)
            where T : ModelBase, new()
        {
            Byte[] modelBinary = null;
            ModelMetadata metadata = model.GetMetadata();

            using (MemoryStream objectGraphStream = new MemoryStream())
            {
                SerializeModel(metadata, knownTypes, objectGraphStream);
                modelBinary = objectGraphStream.ToArray();
            }

            return modelBinary;
        }

        public void SerializeModel(ModelMetadata metadata, IEnumerable<Type> knownTypes, Stream objectGraphStream)
        {
            DataContractSerializer serializer = new DataContractSerializer(typeof(ModelMetadata), knownTypes);
            serializer.WriteObject(objectGraphStream, metadata);
        }

        public T DeserializeModel<T>(Byte[] modelBinary, IEnumerable<Type> knownTypes)
            where T : ModelBase, new()
        {
            T model = null;

            using (MemoryStream objectGraphStream = new MemoryStream(modelBinary))
            {
                ModelMetadata metadata = DeserializeModel(knownTypes, objectGraphStream);
                model = CreateModel<T>(metadata);
            }

            return model;
        }

        public ModelMetadata DeserializeModel(IEnumerable<Type> knownTypes, Stream objectGraphStream)
        {
            DataContractSerializer serializer = new DataContractSerializer(typeof(ModelMetadata), knownTypes);
            return serializer.ReadObject(objectGraphStream) as ModelMetadata;
        }

        public T CreateDeepCopy<T>(T model, IEnumerable<Type> knownTypes)
            where T : ModelBase, new()
        {
            return CreateDeepCopy(model, knownTypes, false);
        }

        public T CreateDeepCopy<T>(T model, IEnumerable<Type> knownTypes, Boolean assignNewUniqueId)
            where T : ModelBase, new()
        {
            Byte[] modelBinary = SerializeModel<T>(model, knownTypes);
            T newModel = DeserializeModel<T>(modelBinary, knownTypes);

            if (assignNewUniqueId)
            {
                newModel.UniqueId = Guid.NewGuid();
            }

            return newModel;
        }

        #endregion

        #region Protected

        protected virtual void InitializeModel<T>(T model, ModelMetadata metadata, ModelPropertyBase parentProperty)
            where T : ModelBase, new()
        {
            model.Initialize(ValidateModelProperties, metadata, parentProperty);
        }

        #endregion

        #region Private

        private ModelBase CreateModel(ModelMetadata metadata)
        {
            Type modelType = Type.GetType(metadata.FullyQualifiedTypeName);
            ConstructorInfo constructor = modelType.GetTypeInfo().DeclaredConstructors.Single(CheckIsDefaultConstructor);
            return constructor.Invoke(null) as ModelBase;
        }

        private static Boolean CheckIsDefaultConstructor(ConstructorInfo constructor)
        {
            return constructor.GetParameters().Count() == 0 && 
                   !constructor.IsStatic; //Make sure it's not the type constructor
        }

        private T InternalCreateModel<T>(ModelMetadata metadata, ModelPropertyBase parentProperty)
            where T : ModelBase, new()
        {
            T model = null;

            if (metadata == null)
            {
                model = new T();
            }
            else
            {
                model = CreateModel(metadata) as T;
            }

            model.PropertyChangeCounter = PropertyChangeCounter;
            model.CommandExceptionHandler = CommandExceptionHandler;
            model.ParentFactory = this;

            if 
            (
                AssignModelUniqueId &&
                model.UniqueId == null //This is necessary to preserve the unique ids of models created from metadata
            )
            {
                model.UniqueId = Guid.NewGuid();
            }

            InitializeModel(model, metadata, parentProperty);
            return model;
        }

        #endregion
    }
}
